package com.young.common.client.log.core;

import com.young.common.client.log.configuration.MonitorLogConfiguration;
import com.young.common.config.TraceInterceptor;
import com.young.common.util.CommonUtil;
import com.young.common.util.LogUtil;
import com.young.common.util.StringUtils;
import com.young.interfaces.log.annotation.APIType;
import com.young.interfaces.log.annotation.Log;
import com.young.interfaces.log.model.LogDTO;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.RejectedExecutionException;

/**
 * web层日志切面
 *
 * @author imrookie
 * @date 2018/11/24
 */
@Aspect
@Configuration
@ConditionalOnBean({MonitorLogConfiguration.class,LogControl.class})
public class LogAop implements Ordered {

    private static final Logger logger = LoggerFactory.getLogger(LogAop.class);

    public static final int DEFAULT_ORDER = 1;//默认切面顺序
    @Autowired
    MonitorLogConfiguration monitorLogConfiguration;

    //切面顺序
    @Override
    public int getOrder() {
        if (monitorLogConfiguration.getAopOrder() == null) {
            return DEFAULT_ORDER;
        }
        return monitorLogConfiguration.getAopOrder();
    }

    /**
     * 切入点
     */
    @Pointcut("@annotation(com.young.interfaces.log.annotation.Log)")
    public void pointcut(){
    }

    /**
     * 环绕通知
     * @param joinPoint
     * @return
     */
    @Around("pointcut()")
    public Object around(JoinPoint joinPoint) throws Throwable {
        LogDTO logDTO = new LogDTO();//日志传输对象
        logDTO.setUuid(CommonUtil.getUUID());//日志唯一编号

        MethodSignature ms = (MethodSignature)joinPoint.getSignature();//获取目标方法的标记
        Method method = ms.getMethod();//目标方法
        Log log = method.getAnnotation(Log.class);//注解信息
        //logDTO.setLog(log);
        Class clz = joinPoint.getTarget().getClass();//所属类

        //处理基础信息
        this.handleBasicInfo(logDTO, log, clz, method);

        //处理web信息
        this.handleWebParam(logDTO, joinPoint, log, clz, method);

        //处理接口执行信息--在执行主体方法前
        this.handleApiParamBeforeProceed(logDTO, log, clz, method, joinPoint);

        //处理链路信息
        this.handleLinkParamBeforeProceed(logDTO);

        Object result = null;
        Throwable throwable = null;
        try {
            //执行此方法
            result = ((ProceedingJoinPoint)joinPoint).proceed();
        } catch (Throwable e) {
            throwable = e;
            throw e;
        } finally {
            //后置接口信息
            this.handleApiParamAfterProceed(logDTO, result, throwable);
            //后置链路信息
            this.handleLinkParamAfterProceed(logDTO);
            try {
                LogControl.catchLog(joinPoint, logDTO);//通过日志控制器异步记录日志
            } catch (RejectedExecutionException e) {//当访问量过大时可能导致异步线程池执行拒绝策略
                logger.error("[日志切面] 异步缓存日志线程池执行拒绝策略(放弃此日志):", e);
            } catch (Throwable e) {
                logger.error("[日志切面] 生产日志发生异常:", e);
            }
        }
        return result;
    }

    /***
     * 是否是web接口
     * @param log
     * @param clz
     * @param method
     * @return
     */
    private boolean isWebApi(Log log, Class clz, Method method){
        if (APIType.contains(log.type(), APIType.WEB)) {
            return true;
        }
        if (APIType.contains(log.type(), APIType.AUTO)) {//自动识别类型
            if (clz.getAnnotation(Controller.class) != null || clz.getAnnotation(RestController.class) != null) {
                if (method.getAnnotation(RequestMapping.class) != null || method.getAnnotation(PostMapping.class) != null || method.getAnnotation(GetMapping.class) != null
                        || method.getAnnotation(DeleteMapping.class) != null || method.getAnnotation(PutMapping.class) != null) {//含有web相关注解
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * 处理基础信息
     * @param logDTO
     * @param log
     * @param clz
     * @param method
     */
    private void handleBasicInfo(LogDTO logDTO, Log log, Class clz, Method method) {
        try {
            logDTO.setAppName(StringUtils.isNotBlank(log.app()) ? log.app() : monitorLogConfiguration.getAppName());//所属应用
            if (StringUtils.isBlank(logDTO.getAppName())) {
                logDTO.setAppName("NON");
            }
            if (StringUtils.isBlank(log.key())) {
                logDTO.setKey(clz.getName() + "." + method.getName());
            } else {
                logDTO.setKey(log.key());
            }
            logDTO.setLogContent(log.value());
            logDTO.setLogHandler(log.handler());
            logDTO.setLogTags(log.tags());
        } catch (Exception e) {
            logger.error("[日志切面-handleBasicInfo] 基础信息处理时异常:", e);
        }
    }

    /**
     * 处理web参数
     * @param logDTO
     * @param joinPoint
     * @param log
     * @param clz
     * @param method
     */
    private void handleWebParam(LogDTO logDTO, JoinPoint joinPoint, Log log, Class clz, Method method) {
        try {
            if (this.isWebApi(log, clz, method)) {
                Object[] args = joinPoint.getArgs();//获取方法参数
                HttpServletRequest request = null;
                ServletRequestAttributes sra = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes());
                if (sra != null){
                    request = sra.getRequest();
                }
                if (request == null){//如果sra.getRequest();的方式没有取到request(正常情况下不会发生),则通过遍历参数寻找
                    logger.error("[日志切面] RequestContextHolder.getRequestAttributes().getRequest()方式获取当前请求的request对象失败,采用反射来获取入参中的request(若入参中没有request则无法获取到)");
                    for(Object obj : args){
                        if (obj != null && obj instanceof HttpServletRequest){
                            request = (HttpServletRequest)obj;
                        }
                    }
                }
                //当参数中存在request时,生成日志所需的参数
                if (request != null){
                    logDTO.setLoginUser(LogUtil.getLoginUser());//用户
                    logDTO.setClientIp(LogUtil.getClientIpAddr(request));//ip地址
                    logDTO.setOs(LogUtil.getClientOS(request));//操作系统
                    logDTO.setBrowser(LogUtil.getClientBrowser(request));//浏览器信息
                    logDTO.setScheme(request.getScheme());//协议,http/https
                    logDTO.setHost(request.getServerName());//host
                    logDTO.setPort(request.getServerPort());//端口号
                    logDTO.setUri(request.getRequestURI());//uri
                    logDTO.setUrl(request.getRequestURL().toString());//请求地址
                    logDTO.setMethod(request.getMethod());//请求方式
                    logDTO.setReferer(LogUtil.getReferer(request));//Referer
                    logDTO.setOrigin(LogUtil.getOrigin(request));//Origin
                    logDTO.setRequestParam(LogUtil.getParameterFromRequest(request));//请求参数
                    logDTO.setParams(logDTO.getRequestParam() != null && !logDTO.getRequestParam().isEmpty() ? new Object[]{logDTO.getRequestParam()} : null);
                }
            }
        } catch (Exception e) {
            logger.error("[日志切面-handleWebParam] web参数处理时异常:", e);
        }
    }

    /**
     * 处理接口执行信息--在执行主体方法前
     * @param logDTO
     * @param log
     * @param clz
     * @param method
     * @param joinPoint
     */
    private void handleApiParamBeforeProceed(LogDTO logDTO, Log log, Class clz, Method method, JoinPoint joinPoint) {
        try {
            logDTO.setClassName(clz.getName());//类名
            logDTO.setMethodName(method.getName());//方法名
            if (!this.isWebApi(log, clz, method)) {//非web接口,记录入参
                Object[] args = joinPoint.getArgs();//获取方法参数
                if (args != null && args.length > 0) {
                    List<Object> argBeans = new ArrayList<Object>(args.length);
                    for (Object obj : args) {
                        //剔除ServletRequest
                        if (obj instanceof ServletRequest) {
                            continue;
                        }
                        argBeans.add(obj);
                    }
                    if (argBeans.size() > 0) {
                        logDTO.setParams(argBeans.toArray());
                    }
                }
            }
            logDTO.setBeginTime(new Date());//开始时间
            LogContextHolder.setLogDTO(logDTO);//设置当前方法的logDTO对象,注意是当前方法,不是当前线程
        } catch (Exception e) {
            logger.error("[日志切面-handleApiParamBeforeProceed] 前置接口参数处理时异常:", e);
        }
    }

    /**
     * 处理接口执行信息--主体方法执行后
     * @param logDTO
     * @param result
     * @param throwable
     */
    private void handleApiParamAfterProceed(LogDTO logDTO, Object result, Throwable throwable) {
        try {
            logDTO.setEndTime(new Date());//结束时间
            logDTO.setResult(result);//返回结果
            if (throwable instanceof Exception){
                logDTO.setException((Exception)throwable);
            }
            LogContextHolder.removeLogDTO(logDTO);//移除当前方法的日志对象,注意是当前方法,不是当前线程
        } catch (Exception e) {
            logger.error("[日志切面-handleApiParamBeforeProceed] 后置接口参数处理时异常:", e);
        }
    }

    /**
     * 处理链路信息--主体执行前
     * @param logDTO
     */
    private void handleLinkParamBeforeProceed(LogDTO logDTO) {
        try {
            String traceId = TraceInterceptor.getTraceId();//从追踪拦截器获取traceId
            if (StringUtils.isBlank(traceId)) {
                traceId = CommonUtil.getUUID();
            }
            logDTO.setTraceId(traceId);//记录追踪id
        } catch (Exception e) {
            logger.error("[日志切面-handleApiParamBeforeProceed] 前置链路参数处理时异常:", e);
        }
    }

    /**
     * 处理链路信息--主体执行后
     * @param logDTO
     */
    private void handleLinkParamAfterProceed(LogDTO logDTO) {

    }

    /*@Before("pointcut()")
    public void before(JoinPoint joinPoint){
        System.out.println("111-前置通知");
    }

    @After("pointcut()")
    public void after(JoinPoint joinPoint){
        System.out.println("111-后置通知");
    }
    @Around("pointcut()")
    public Object around(JoinPoint joinPoint){
        Object object = null;
        System.out.println("111-环绕start");
        try {
            object = ((ProceedingJoinPoint)joinPoint).proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        System.out.println("111-环绕over");
        return object;
    }
    @AfterReturning("pointcut()")
    public void afterReturn(){
        System.out.println("111-AfterReturning");
    }
    @AfterThrowing(value = "pointcut()", throwing = "ex")
    public void afterThrowing(Exception ex){
        System.out.println("111-AfterThrowing");
    }*/
}
